import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';

const selectors = {
    canvas: '#viewer',
    dropzone: '#dropzone',
    fileInput: '#file-input',
    status: '#status',
    pointCount: '#point-count',
    fileName: '#file-name',
    bounds: '#bounds',
    pointSize: '#point-size',
    background: '#background',
    colormap: '#colormap',
    gridToggle: '#show-grid',
    bboxToggle: '#show-bbox',
    resetBtn: '#reset-view',
};

const state = {
    scene: null,
    camera: null,
    renderer: null,
    controls: null,
    pointObject: null,
    data: null,
    bboxHelper: null,
    gridHelper: null,
    frameId: null,
    statusEl: null,
    canvas: null,
    pointSize: 0.1,
    colorMode: 'height',
};

const loaders = {
    ply: new PLYLoader(),
    pcd: new PCDLoader(),
};

document.addEventListener('DOMContentLoaded', init);

function init() {
    state.canvas = query(selectors.canvas);
    state.statusEl = query(selectors.status);
    initScene();
    bindEvents();
    renderLoop();
    updateStatus('等待文件...');
}

function initScene() {
    const viewport = state.canvas.parentElement;
    state.renderer = new THREE.WebGLRenderer({
        canvas: state.canvas,
        antialias: true,
        alpha: true,
    });
    state.renderer.setPixelRatio(window.devicePixelRatio);
    state.renderer.setSize(viewport.clientWidth, viewport.clientHeight);

    state.scene = new THREE.Scene();
    state.scene.background = new THREE.Color('#111827');

    const fov = 60;
    const aspect = viewport.clientWidth / viewport.clientHeight;
    state.camera = new THREE.PerspectiveCamera(fov, aspect, 0.1, 5000);
    state.camera.position.set(30, 25, 30);

    state.controls = new OrbitControls(state.camera, state.canvas);
    state.controls.target.set(0, 0, 0);
    state.controls.enableDamping = true;

    const ambient = new THREE.AmbientLight(0xffffff, 0.5);
    const dir = new THREE.DirectionalLight(0xffffff, 0.6);
    dir.position.set(30, 50, 10);
    state.scene.add(ambient, dir);

    state.gridHelper = new THREE.GridHelper(100, 50, 0x334155, 0x1e293b);
    state.scene.add(state.gridHelper);

    const axes = new THREE.AxesHelper(10);
    axes.material.depthTest = false;
    axes.renderOrder = 999;
    state.scene.add(axes);

    window.addEventListener('resize', () => resizeRenderer(viewport));
}

function bindEvents() {
    const dropzone = query(selectors.dropzone);
    dropzone.addEventListener('dragover', (event) => {
        event.preventDefault();
        dropzone.classList.add('dragover');
    });
    dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
    dropzone.addEventListener('drop', (event) => {
        event.preventDefault();
        dropzone.classList.remove('dragover');
        const file = event.dataTransfer?.files?.[0];
        if (file) handleFile(file);
    });

    query(selectors.fileInput).addEventListener('change', (event) => {
        const file = event.target.files?.[0];
        if (file) handleFile(file);
    });

    query(selectors.pointSize).addEventListener('input', (event) => {
        state.pointSize = parseFloat(event.target.value);
        updatePointSize();
    });

    query(selectors.background).addEventListener('change', (event) => {
        state.scene.background = new THREE.Color(event.target.value);
    });

    query(selectors.colormap).addEventListener('change', (event) => {
        state.colorMode = event.target.value;
        applyColorMode();
    });

    query(selectors.gridToggle).addEventListener('change', (event) => {
        state.gridHelper.visible = event.target.checked;
    });

    query(selectors.bboxToggle).addEventListener('change', (event) => {
        if (state.bboxHelper) state.bboxHelper.visible = event.target.checked;
    });

    query(selectors.resetBtn).addEventListener('click', () => resetView());

}

function handleFile(file) {
    const name = file.name;
    const ext = name.split('.').pop()?.toLowerCase();
    if (!ext) {
        updateStatus('无法识别的文件类型');
        return;
    }
    updateStatus(`读取 ${name} ...`);

    if (ext === 'pcd') {
        readAsArrayBuffer(file).then((buffer) => parsePCD(buffer, name)).catch(showError);
    } else if (ext === 'ply') {
        readAsArrayBuffer(file).then((buffer) => parsePLY(buffer, name)).catch(showError);
    } else if (ext === 'bin') {
        readAsArrayBuffer(file).then((buffer) => parseBIN(buffer, name)).catch(showError);
    } else if (['xyz', 'txt', 'csv'].includes(ext)) {
        readAsText(file).then((text) => parseTextPoints(text, name)).catch(showError);
    } else {
        updateStatus('暂不支持该文件类型');
    }
}

function parsePLY(buffer, name) {
    try {
        const geometry = loaders.ply.parse(buffer);
        geometry.computeBoundingBox();
        normalizePLYColorsIfNeeded(geometry);
        const data = geometryToData(geometry);
        showPointCloud(data, name);
    } catch (error) {
        showError(error);
    }
}

function parsePCD(buffer, name) {
    try {
        const points = loaders.pcd.parse(buffer, name, true);
        const geometry = points.geometry;
        geometry.computeBoundingBox();
        const data = geometryToData(geometry);
        showPointCloud(data, name);
    } catch (error) {
        showError(error);
    }
}

function parseBIN(buffer, name) {
    const floats = toLittleEndianFloat32(buffer);
    if (floats.length < 3) {
        updateStatus('BIN 文件内容不足以形成点云');
        return;
    }

    const stride = detectBinStride(floats);
    const pointCount = Math.floor(floats.length / stride);
    if (!pointCount) {
        updateStatus('未解析到有效点');
        return;
    }

    const remainder = floats.length % stride;
    if (remainder !== 0) {
        console.warn(`BIN 浮点数 ${floats.length} 无法被 ${stride} 整除，忽略末尾 ${remainder} 个分量`);
    }

    const layoutLabel = describeBinLayout(stride);
    updateStatus(`推断 BIN 布局：${layoutLabel}，共 ${pointCount.toLocaleString()} 点`);

    const positions = new Float32Array(pointCount * 3);
    const colors = stride >= 6 ? new Float32Array(pointCount * 3) : null;
    const intensity = stride === 4 ? new Float32Array(pointCount) : null;

    for (let i = 0; i < pointCount; i++) {
        const base = i * stride;
        positions[i * 3] = floats[base];
        positions[i * 3 + 1] = floats[base + 1];
        positions[i * 3 + 2] = floats[base + 2];

        if (colors) {
            let r = floats[base + 3];
            let g = floats[base + 4];
            let b = floats[base + 5];
            if (r > 1 || g > 1 || b > 1) {
                r /= 255;
                g /= 255;
                b /= 255;
            }
            colors[i * 3] = r;
            colors[i * 3 + 1] = g;
            colors[i * 3 + 2] = b;
        } else if (intensity) {
            intensity[i] = floats[base + 3];
        }
    }

    const data = { positions, colors, intensity };
    computeBounds(data);
    showPointCloud(data, name);
}

function detectBinStride(floatArray) {
    const length = floatArray.length;
    const divisibleBy4 = length % 4 === 0;
    const divisibleBy6 = length % 6 === 0;

    if (divisibleBy4 && divisibleBy6) {
        const rgbConfidence = estimateRgbConfidence(floatArray);
        return rgbConfidence >= 0.55 ? 6 : 4;
    }
    if (divisibleBy6) return 6;
    if (divisibleBy4) return 4;
    if (length % 3 === 0) return 3;
    return length >= 4 ? 4 : 3;
}

function describeBinLayout(stride) {
    if (stride === 6) return 'XYZ + RGB (float)';
    if (stride === 4) return 'XYZ + 强度 (float)';
    return 'XYZ (float)';
}

function estimateRgbConfidence(floatArray) {
    const sampleStride = 6;
    const totalPoints = Math.floor(floatArray.length / sampleStride);
    const sampleCount = Math.min(2000, totalPoints);
    if (!sampleCount) return 0;

    let rgbLike = 0;
    for (let i = 0; i < sampleCount; i++) {
        const base = i * sampleStride + 3;
        const r = floatArray[base];
        const g = floatArray[base + 1];
        const b = floatArray[base + 2];
        if (isLikelyColorChannel(r) && isLikelyColorChannel(g) && isLikelyColorChannel(b)) {
            const nonNegative = r >= -0.05 && g >= -0.05 && b >= -0.05;
            if (nonNegative) rgbLike++;
        }
    }
    return rgbLike / sampleCount;
}

function isLikelyColorChannel(value) {
    if (!Number.isFinite(value)) return false;
    const abs = Math.abs(value);
    const withinUnitRange = abs <= 1.2;
    const withinByteRange = value >= 0 && abs <= 255;
    return withinUnitRange || withinByteRange;
}

function toLittleEndianFloat32(buffer) {
    const remainder = buffer.byteLength % 4;
    if (remainder !== 0) {
        console.warn(`BIN 文件字节数 (${buffer.byteLength}) 不是 4 的整数倍，忽略结尾 ${remainder} 字节`);
    }
    const length = Math.floor(buffer.byteLength / 4);
    const floats = new Float32Array(length);
    const view = new DataView(buffer);
    for (let i = 0; i < length; i++) {
        floats[i] = view.getFloat32(i * 4, true);
    }
    return floats;
}

function parseTextPoints(text, name) {
    const lines = text.split(/\r?\n/);
    const positions = [];
    const colors = [];
    const intensity = [];
    let hasRGB = false;
    let hasIntensity = false;

    for (const raw of lines) {
        const line = raw.trim();
        if (!line || line.startsWith('#')) continue;
        const parts = line.split(/[\s,;]+/).map(Number).filter((value) => Number.isFinite(value));
        if (parts.length < 3) continue;
        const [x, y, z] = parts;
        positions.push(x, y, z);

        if (parts.length >= 6) {
            let [r, g, b] = parts.slice(3, 6);
            if (r > 1 || g > 1 || b > 1) {
                r /= 255;
                g /= 255;
                b /= 255;
            }
            colors.push(r, g, b);
            hasRGB = true;
        } else if (parts.length === 4) {
            intensity.push(parts[3]);
            hasIntensity = true;
        } else {
            colors.push(0.8, 0.8, 0.8);
        }
    }

    if (!positions.length) {
        updateStatus('未解析到有效点');
        return;
    }

    const data = {
        positions: new Float32Array(positions),
        colors: hasRGB ? new Float32Array(colors) : null,
        intensity: hasIntensity ? new Float32Array(intensity) : null,
    };
    computeBounds(data);
    showPointCloud(data, name);
}

function geometryToData(geometry) {
    const positions = geometry.getAttribute('position')?.array;
    const colorsAttr = geometry.getAttribute('color');
    const intensityAttr = geometry.getAttribute('intensity');

    const data = {
        positions: new Float32Array(positions),
        colors: colorsAttr ? normalizeColorArray(colorsAttr.array) : null,
        intensity: intensityAttr ? new Float32Array(intensityAttr.array) : null,
    };
    computeBounds(data);
    return data;
}

function normalizePLYColorsIfNeeded(geometry) {
    const colorsAttr = geometry.getAttribute('color');
    if (!colorsAttr) return;
    const array = colorsAttr.array;
    let needsNormalize = false;
    for (let i = 0; i < array.length; i++) {
        if (array[i] > 1) {
            needsNormalize = true;
            break;
        }
    }
    if (needsNormalize) {
        for (let i = 0; i < array.length; i++) {
            array[i] /= 255;
        }
    }
}

function normalizeColorArray(array) {
    const normalized = new Float32Array(array.length);
    let shouldNormalize = false;
    for (let i = 0; i < array.length; i++) {
        if (array[i] > 1) {
            shouldNormalize = true;
            break;
        }
    }
    if (shouldNormalize) {
        for (let i = 0; i < array.length; i++) {
            normalized[i] = array[i] / 255;
        }
    } else {
        normalized.set(array);
    }
    return normalized;
}

function showPointCloud(data, name) {
    state.data = data;
    if (state.pointObject) {
        state.scene.remove(state.pointObject);
        state.pointObject.geometry.dispose();
        state.pointObject.material.dispose();
    }

    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.BufferAttribute(state.data.positions, 3));
    if (state.data.colors) {
        geometry.setAttribute('color', new THREE.BufferAttribute(state.data.colors, 3));
    }

    const material = new THREE.PointsMaterial({
        size: state.pointSize,
        vertexColors: !!state.data.colors,
        sizeAttenuation: true,
    });

    state.pointObject = new THREE.Points(geometry, material);
    state.scene.add(state.pointObject);

    addBoundingBox();
    resetView();
    updateSidebar(name);
    applyColorMode();
    updatePointSize();
    const count = state.data.positions.length / 3;
    updateStatus(`解析完成 ✅（${count.toLocaleString()} 点）`);
}

function applyColorMode() {
    if (!state.pointObject || !state.data) return;

    const geometry = state.pointObject.geometry;
    const count = geometry.getAttribute('position').count;
    let colors = geometry.getAttribute('color')?.array;

    if (!colors || colors.length !== count * 3) {
        colors = new Float32Array(count * 3);
        geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    }

    if (state.colorMode === 'single') {
        for (let i = 0; i < count; i++) {
            colors[i * 3] = 0.8;
            colors[i * 3 + 1] = 0.9;
            colors[i * 3 + 2] = 1;
        }
    } else if (state.colorMode === 'intensity' && (state.data.intensity || state.data.colors)) {
        const intensity = state.data.intensity ?? computeLuminance(state.data.colors);
        const [minI, maxI] = getRange(intensity);
        for (let i = 0; i < count; i++) {
            const value = normalizeValue(intensity[i], minI, maxI);
            colors[i * 3] = value;
            colors[i * 3 + 1] = value;
            colors[i * 3 + 2] = value;
        }
    } else {
        const positions = geometry.getAttribute('position').array;
        const [minZ, maxZ] = state.data.bounds?.z ?? [-1, 1];
        for (let i = 0; i < count; i++) {
            const z = positions[i * 3 + 2];
            const value = normalizeValue(z, minZ, maxZ);
            const color = heightGradient(value);
            colors[i * 3] = color.r;
            colors[i * 3 + 1] = color.g;
            colors[i * 3 + 2] = color.b;
        }
    }

    geometry.getAttribute('color').needsUpdate = true;
    state.pointObject.material.vertexColors = true;
}

function updatePointSize() {
    if (!state.pointObject) return;
    state.pointObject.material.size = state.pointSize;
    state.pointObject.material.needsUpdate = true;
}

function updateSidebar(name) {
    query(selectors.fileName).textContent = name;
    const count = state.data.positions.length / 3;
    query(selectors.pointCount).textContent = count.toLocaleString();
    const zRange = state.data.bounds?.z;
    const xRange = state.data.bounds?.x;
    const yRange = state.data.bounds?.y;
    const text = [
        `X: ${formatRange(xRange)}`,
        `Y: ${formatRange(yRange)}`,
        `Z: ${formatRange(zRange)}`,
    ].join(' | ');
    query(selectors.bounds).textContent = text;
}

function addBoundingBox() {
    if (!state.pointObject) return;
    if (state.bboxHelper) {
        state.scene.remove(state.bboxHelper);
    }
    const box = new THREE.Box3().setFromObject(state.pointObject);
    state.bboxHelper = new THREE.Box3Helper(box, 0x38bdf8);
    state.bboxHelper.visible = query(selectors.bboxToggle).checked;
    state.scene.add(state.bboxHelper);
}

function resetView() {
    if (!state.pointObject) return;
    const box = new THREE.Box3().setFromObject(state.pointObject);
    const size = box.getSize(new THREE.Vector3()).length();
    const center = box.getCenter(new THREE.Vector3());

    state.controls.target.copy(center);
    state.controls.update();

    const distance = size * 1.2;
    state.camera.position.copy(center);
    state.camera.position.x += distance;
    state.camera.position.y += distance * 0.6;
    state.camera.position.z += distance;
    state.camera.updateProjectionMatrix();
    state.controls.update();
}

function resizeRenderer(viewport) {
    if (!state.renderer) return;
    const { clientWidth, clientHeight } = viewport;
    state.renderer.setSize(clientWidth, clientHeight);
    state.camera.aspect = clientWidth / clientHeight;
    state.camera.updateProjectionMatrix();
}

function renderLoop() {
    state.frameId = requestAnimationFrame(renderLoop);
    if (state.controls) state.controls.update();
    if (state.renderer && state.scene && state.camera) {
        state.renderer.render(state.scene, state.camera);
    }
}

function formatRange(range) {
    if (!range) return '-';
    return `${range[0].toFixed(2)} ~ ${range[1].toFixed(2)}`;
}

function updateStatus(message) {
    if (state.statusEl) {
        state.statusEl.textContent = message;
    }
}

function showError(error) {
    console.error(error);
    updateStatus(`解析失败：${error.message ?? error}`);
}

function computeBounds(data) {
    const arr = data.positions;
    const bounds = {
        x: [Infinity, -Infinity],
        y: [Infinity, -Infinity],
        z: [Infinity, -Infinity],
    };
    for (let i = 0; i < arr.length; i += 3) {
        const x = arr[i];
        const y = arr[i + 1];
        const z = arr[i + 2];
        bounds.x[0] = Math.min(bounds.x[0], x);
        bounds.x[1] = Math.max(bounds.x[1], x);
        bounds.y[0] = Math.min(bounds.y[0], y);
        bounds.y[1] = Math.max(bounds.y[1], y);
        bounds.z[0] = Math.min(bounds.z[0], z);
        bounds.z[1] = Math.max(bounds.z[1], z);
    }
    data.bounds = bounds;
}

function computeLuminance(colors) {
    const len = colors.length / 3;
    const luminance = new Float32Array(len);
    for (let i = 0; i < len; i++) {
        const r = colors[i * 3];
        const g = colors[i * 3 + 1];
        const b = colors[i * 3 + 2];
        luminance[i] = 0.299 * r + 0.587 * g + 0.114 * b;
    }
    return luminance;
}

function getRange(array) {
    if (!array) return [0, 1];
    let min = Infinity;
    let max = -Infinity;
    for (let i = 0; i < array.length; i++) {
        const value = array[i];
        if (value < min) min = value;
        if (value > max) max = value;
    }
    if (min === max) {
        max = min + 1;
    }
    return [min, max];
}

function normalizeValue(value, min, max) {
    return (value - min) / (max - min || 1);
}

function heightGradient(t) {
    const stops = [
        { stop: 0, color: [0.231, 0.298, 0.753] },
        { stop: 0.25, color: [0.283, 0.417, 0.878] },
        { stop: 0.5, color: [0.865, 0.865, 0.865] },
        { stop: 0.75, color: [0.953, 0.631, 0.365] },
        { stop: 1, color: [0.706, 0.016, 0.150] },
    ];
    for (let i = 0; i < stops.length - 1; i++) {
        const curr = stops[i];
        const next = stops[i + 1];
        if (t >= curr.stop && t <= next.stop) {
            const local = (t - curr.stop) / (next.stop - curr.stop || 1);
            return {
                r: THREE.MathUtils.lerp(curr.color[0], next.color[0], local),
                g: THREE.MathUtils.lerp(curr.color[1], next.color[1], local),
                b: THREE.MathUtils.lerp(curr.color[2], next.color[2], local),
            };
        }
    }
    return { r: 1, g: 1, b: 1 };
}

function readAsArrayBuffer(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onerror = () => reject(new Error('文件读取失败'));
        reader.onload = () => resolve(reader.result);
        reader.readAsArrayBuffer(file);
    });
}

function readAsText(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onerror = () => reject(new Error('文件读取失败'));
        reader.onload = () => resolve(reader.result);
        reader.readAsText(file);
    });
}

function query(selector) {
    return document.querySelector(selector);
}
